
/*
 * Copyright (C) 2010 Bitglue. All rights reserved.
 *
 * $Id: UrlScanner.java bself $
 */

package com.bitglue.osgi.scan;

import com.bitglue.osgi.model.Deployment;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.ContentEncodingHttpClient;

import org.cyberneko.html.parsers.DOMParser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.util.StopWatch;

import org.w3c.dom.Node;
import org.w3c.dom.html.HTMLAnchorElement;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * UrlScanner scans target url and parses the directory
 * ultimately keeping the contents in sync with a local
 * repository and database. This can be the framework
 * for allowing deployable resources.
 * @author Bryan Patrick Self
 */
public class UrlScanner extends BaseScanner {

    protected static final String PAT
      = "(.*)[/]([^/]+)([/]?)$|([^/]+)([/]?)$";
    protected static final String PARENT = "..";
    protected HttpClient client;
    protected Pattern pattern;
    private final Logger log = LoggerFactory.getLogger(getClass());

    public UrlScanner(String[] exts) {
        super(exts);
        pattern = Pattern.compile(PAT);
    }

    public void scan() {
        if (!plugable.isReady() || !enabled)
            return;
        log.trace("url scanner initiating scan on " + scanTarget);
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try {
            client = new ContentEncodingHttpClient();
            if (!getDirectory(new HttpGet(scanTarget)))
                return;
            mergeDatabase();
        } finally {
            client.getConnectionManager().shutdown();
        }
        stopWatch.stop();
        log.trace("url scanner completed scan: "
          + stopWatch.shortSummary());
    }

    public boolean getDirectory(HttpGet httpget) {
        String uri = httpget.getURI().toString();
        log.trace("running GET on " + uri);
        try {
            HttpResponse response = client.execute(httpget);
            int code = response.getStatusLine().getStatusCode();
            if (code != HttpStatus.SC_OK) {
                log.debug(uri + " gave " + response.getStatusLine());
                return false;
            }
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                log.warn("missing entity attached to response");
                return false;
            }
            DOMParser parser = new DOMParser();
            parser.parse(new InputSource(entity.getContent()));
            parseAnchors(parser.getDocument(), uri);
            return true;
        } catch (IOException io) {
             log.warn("I/O exception with GET on "
               + uri + "->" + io.getMessage());
        } catch (SAXException se) {
             log.warn("failed to parse HTML on "
               + uri + "->" + se.getMessage());
             httpget.abort();
        } catch (Exception ex) {
             log.warn("http runtime exception on "
               + uri + httpget.getURI() + "->" + ex);
             httpget.abort();
        }
        return false;
    }

    protected void parseAnchors(Node node, String uri) {
        if (node instanceof HTMLAnchorElement) {
            HTMLAnchorElement anchor = (HTMLAnchorElement)node;
            String ref = anchor.getHref();
            Matcher m = pattern.matcher(ref);
            if (m.find())
                procReference(m, uri, ref);
        }
        Node child = node.getFirstChild();
        while (child != null) {
            parseAnchors(child, uri);
            child = child.getNextSibling();
        }
    }

    protected void procReference(Matcher m, String uri, String ref) {
        String leaf = m.group(2);
        String end = m.group(3);
        String path = null;
        try {
            path = new URI(uri).getPath();
        } catch (URISyntaxException ex) {
            log.warn("invalid uri constructed: " + uri + "->" + ex);
            return;
        }
        if (m.group(4) == null && m.group(5) == null) {
            if (path.startsWith(ref) || PARENT.equals(leaf))
                return;
        } else {
            leaf = m.group(4);
            end = m.group(5);
        }
        String src = uri + "/" + leaf;
        if ("/".equals(end) && recursive) {
            if (!getDirectory(new HttpGet(src)))
                throw new RuntimeException("failed to recurse " + src);
            return;
        }
        Deployment d = parseResource(leaf);
        if (d == null)
            return;
        d.setSource(src);
        Deployment tmp = batch.get(d.getModule());
        if (tmp != null) {
            if (isVersionLessOrEqualTo(d, tmp.getVersion())) {
                log.debug("ignoring module " + d.getModule()
                  + " @ " + d.getVersion() + " since "
                  + "duplicate mod version >= " + tmp.getVersion());
                return;
            } else {
                log.debug("ignoring module " + tmp.getModule()
                  + " @ " + tmp.getVersion() + " since "
                  + "duplicate mod version > " + d.getVersion());
                FileUtils.deleteQuietly(new File(tmp.getRepoEntry()));
            }
        }
        log.trace("adding deployment resource of " + leaf);
        if (acquireResource(src, leaf)) {
            d.setRepoEntry(repository + "/" + leaf);
            batch.put(d.getModule(), d);
        }
    }

}
